\chapter{操作系统介绍}
\thispagestyle{empty}

如果你已经上过大学的操作系统课程，你就应该已经知道当计算机运行的时候程序的一些运作原理。如果没有的话，这本书（以及对应的课程）对于你而言可能会很难——所以你最好停止阅读本书，然后去最近的书店买一本相关背景的书学习之后再继续（Patt/Patel [PP03] 特别是 Bryant/O’Hallaron [BOH10] 都是很好的书）。

那么当一个程序在运行的时候发生了什么呢？

其实一个运行的程序只做了一个非常简单的事情：它执行了指令。每秒钟会有数百万条指令在执行（现在可能是数十亿条），处理器从内存中\textbf{取出}指令，\textbf{解码}（确定执行的行为），然后\textbf{执行}这条指令（它做一些设定好的工作，例如两个数字相加，访问内存，检查条件，跳转至一个函数等等）。在执行完这条指令之后，处理器开始执行下一条指令，一直如此，直到程序执行完成\footnote{当然，现代处理器在底层用了很多黑科技的技巧让程序运行的更快，例如：一次执行多条指令，然后按指令的输入顺序完成！但这不是我们这里所关心的。我们只关心大多数程序假设的简单模型：这些指令看起来以有序且连续的方式，一次执行一条指令}。

因此，我们只需描述基础的\textbf{冯.诺依曼}的计算机模型\footnote{冯.诺依曼是早期计算系统的先驱之一。他还做过关于博弈论和原子弹的开创性工作，并且在 NBA 打了6年的球。好吧，其中有一件事情是假的。}。听着很简单，对吧？但在这堂课中，我们需要学习当一个程序运行的时候，很多其他东西运行以达成系统\textbf{易于使用}这个主要目标。

这是一整套软件体系，实际上，这是让其可靠的更易于运行多个程序（即使是表面上让你感觉是同时运行的），运行程序共享内存，根据设备动态调整程序，或者类似于这些的有趣的东西。这个整套的软件体系被称为\textbf{操作系统(OS)}\footnote{早期操作系统也被称之为\textbf{监督者}或\textbf{管理控制程序}。显然后者听起来太热情了（可以看电影『创·战纪』了解详情），幸亏操作系统流行起来并且取代了后者}，它是来保证整个系统操作正确且有效又易于使用的方式。

\begin{tcolorbox}[colframe=grey,colback= grey,arc=0pt,left=6pt,right=6pt,top=6pt,bottom=6pt,boxsep=0pt]
\begin{center}
问题的关键\\
如何虚拟化资源
\end{center}
我们会在书中简洁明快的回答这个中心问题：操作系统如何虚拟化资源？这是我们问题的核心。为什么这是操作系统的主要问题，答案显而易见：虚拟化让系统更易于使用，那么，让我们专注于怎么做：操作系统以什么机制和策略来实现虚拟化？操作系统如何高效实现这个功能？哪些硬件功能是需要支持的？

我们将使用『问题的关键』，像这样一个黑盒子，来整理我们在构建一个操作系统时遇到的所有问题。因此在这个特殊主题的笔记里，你会发现一个或更多的重量级难题（当然, 是适当数量）。当然，在这一章的详情中，有这些难题的解决方案，或者至少有基本的解决思路。
\end{tcolorbox}

操作系统实现这些需求的主要方法是被我们称之为\textbf{虚拟化}的技术。简而言之，操作系统调用\textbf{硬件}资源（例如处理器，内存或磁盘）然后将其转化为更通用，强大且易于使用的\textbf{虚拟}形式。因此，我们有时候将操作系统引申为\textbf{虚拟机}。

当然，为了让用户能够告诉操作系统做什么以及使用虚拟机的特性（例如运行程序，分配内存或访问文件），操作系统也支持一些API接口给用户调用。事实上，一个典型的操作系统会给出数百个\textbf{系统调用}以供应用程序使用。因为系统支持这些调用来运行程序，访问内存、设备，以及其他的操作。我们也将这些称之为系统给程序提供的\textbf{标准库}。

最后，因为虚拟化允许多个程序同时运行（即共享CPU），同时访问他们自己的结构和数据（即共享内存），以及同时访问设备（即共享磁盘等等），所以操作系统有时被认为是\textbf{资源管理器}。每个CPU，内存和磁盘都是系统的\textbf{资源}；因此，操作系统的\textbf{角色}就是管理这些资源，有效或者公平或者考虑到其他可能的目标的实现。为了更好的理解操作系统的角色，让我们看一些例子。

\newpage

\begin{figure}[ht]
\begin{lstlisting}  
#include <stdio.h> 
#include <stdlib.h> 
#include <sys/time.h> 
#include <assert.h> 
#include "common.h"
int
main(int argc, char *argv[])
{
    if (argc != 2) {
        fprintf(stderr, "usage: cpu <string>\n");
        exit(1);
    }
    char *str = argv[1];
    while (1) {
        Spin(1);
        printf("%s\n", str);
    }
    return 0;
}
\end{lstlisting}
\caption{\textbf{简单示例：有关循环及打印的代码}}
\end{figure}

\section{虚拟化CPU}
    
图2.1描述了我们的第一段程序。它并没有做什么。实际上这段程序做的只是调用 Spin() 这个函数，一旦运行了一秒钟，就重复检查时间并返回。然后打印出用户在命令行上传入的字符串，并且一直重复。

我们将文件保存为 cpu.c 并且编译然后通过一个单独处理器在系统上运行（或者有时候我们称之为 \textbf{CPU}）。接下来我们会看到以下效果。

\begin{verbatim}
prompt> gcc -o cpu cpu.c -Wall
prompt> ./cpu "A"
A
A
A
A
ˆC
prompt>
\end{verbatim}

不是太有趣的运行过程——系统开始运行程序，循环检查时间直到经过一秒。一旦经过一秒，代码打印出用户输入的字符（在这个例子中，就是字母 A），然后继续。注意程序会一直运行下去；只有按下“Control-C”（在基于 UNIX 的系统上就会终止前台运行的程序）才能中断程序。

现在让我们做相同的事情，但这次我们运行这个程序的多个不同的实例。图2.2显示了这个稍微复杂例子的结果。

\begin{figure}[ht]
\begin{verbatim}
prompt> ./cpu A & ; ./cpu B & ; ./cpu C & ; ./cpu D &
[1] 7353
[2] 7354
[3] 7355
[4] 7356
A
B
D
C
A
B
D
C
A
C
B
D
...
\end{verbatim}
\caption{\textbf{同时运行多个程序}}
\end{figure}

好了，现在事情变得更有趣了。尽管我们只有一个处理器，但我们的四个程序却同时运行了！这样的魔法是怎么发生的呢？\footnote{注意：我们通过 \& 符号同时运行四个程序的。这样做会在 tcsh shell 的后台运行一份作业，这意味着用户可以立即发布下一个命令，在这个情况下就是运行另一个程序。命令之间的分号允许我们在 tcsh 中同时运行多个程序。如果你使用的是不同的 shell（例如：bash），那工作机制可能会有略微的不同；想了解更多信息要阅读相关的文档。}

事实证明，在硬件的部分协助下，会有操作系统负责一切的假象，即：系统有很多的虚拟 CPU。将一个（或少量） CPU 转换为看似有无数的 CPU，从而使许多程序看似能够一同运行的技术，我们将其称之为虚拟化 CPU，这也是我们书中所要讲的第一个主要的焦点。

当然，运行程序或终止程序，又或者告诉系统哪些程序需要运行，这些都需要通过接口（APIs）来将你的想法传达给操作系统。实际上，这是大多数用户和操作系统沟通的主要方法；针对这些 APIs 的探讨会贯穿我们的整本书。

你可能也会注意到，一次性运行多个程序的功能会导致各种新的问题。例如，当两个程序都希望在特定的时间运行，那哪一个程序应该运行？这个问题要由操作系统的\textbf{策略}来解答；在操作系统的很多不同的地方都使用策略解决这类问题，我们将在了解操作系统实现的基本\textbf{机制}时来研究它们（例如同时运行多个程序的功能）。因此，操作系统的角色是一个\textbf{资源管理器}。

\begin{figure}[ht]
\begin{lstlisting}  
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include "common.h"

int
main(int argc, char *argv[])
{
    int *p = malloc(sizeof(int)); // a1 
    assert(p != NULL);
    printf("(%d) address of p: %08x\n",
            getpid(), (unsigned) p); // a2
    *p = 0; // a3
    while (1) {
        Spin(1);
        *p = *p + 1;
        printf("(%d) p: %d\n", getpid(), *p); // a4
    }
    return 0;
} 
\end{lstlisting}
\caption{\textbf{一段访问内存的程序}}
\end{figure}

\section{虚拟化内存}
现在我们考虑内存。现代机器提供的\textbf{物理内存}模型是非常简单的。内存只是一组 bytes；要\textbf{读取}内存，必须指定一个地址才能访问存储在那里的数据。要\textbf{写入}（或\textbf{更新}）内存，还必须指定要写入给定地址的数据。

内存在程序运行的时候一直在被访问。一个程序将其所有的数据结构保存在内存中，然后通过各种指令来访问。例如加载、存储以及其他明确的指令来访问内存以完成工作。别忘了程序的每个指令也在内存中；因此每次取指令也会访问内存。

让我们看一段通过调用 malloc() 来分配一些内存的程序（图2.3）。这段程序的输入如下：
\begin{verbatim}
prompt> ./mem
(2134) memory address of p: 00200000
(2134) p: 1
(2134) p: 2
(2134) p: 3
(2134) p: 4
(2134) p: 5
ˆC
\end{verbatim}

这段程序做了几个事情。首先，它分配了一些内存（a1行）。然后打印出内存地址（a2行），再之后，将数字0置入新分配内存的第一个插槽中（a3行）。最终，不断循环延迟一秒钟并递增存储在 p 中保存的地址处的值。对于每个打印语句还会打印出正在运行程序的进程id（PID）。该 PID 在每个运行的进程中都是唯一的。

又是这样，首次的结果并不太有趣。新的内存分配地址是 00200000。程序运行后，它会慢慢更新值并打印出结果。

\begin{figure}[ht]
\begin{verbatim}
prompt> ./mem &; ./mem &
[1] 24113
[2] 24114
(24113) memory address of p: 00200000
(24114) memory address of p: 00200000
(24113) p: 1
(24114) p: 1
(24114) p: 2
(24113) p: 2
(24113) p: 3
(24114) p: 3
(24113) p: 4
(24114) p: 4
... 
\end{verbatim}
\caption{\textbf{多次运行中的内存程序}}
\end{figure}

现在，我们再次运行这个程序的多个实例看看会发生什么（图2.4）。我们从示例中看到，每个正在运行的程序都在相同的地址（00200000）处分配了内存，而且似乎每个程序都在独立的更新 00200000 的值！就好像每个正在运行的程序都有自己的私有内存，而不是与其他正在运行的程序共享相同的物理内存\footnote{为了确保这个例子能够运行成功，您需要禁用随机化地址空间；事实证明，随机化可以很好的防御某些安全漏洞。如果你想了解更多，特别是学习如何通过堆栈溢出来黑进计算机系统。当然，我们不推荐这样的事情……}。

事实上，这正是操作系统\textbf{虚拟化内存}时发生的情况。每个进程访问自己的私有\textbf{虚拟地址空间}（有时也称为\textbf{地址空间}），操作系统将其以某种方式映射到机器的物理内存上。一个正在运行的程序中的内存引用不会影响其他进程（或操作系统本身）的地址空间；就正在运行的程序而言，它本身都有自己的物理内存。然而真实情况是，物理内存是由操作系统管理的共享资源。所有这些是如何完成的，这也是本书第一部分关于\textbf{虚拟化}议题的主题。

\section{并发性}
本书的另外一个主题是\textbf{并发性}。我们使用这个概念术语指代出现的大量的，并且在同一个程序中一次（即同时）处理许多事情时必须加以解决的问题。并发性问题首先出现在操作系统本身；正如你在上面关于虚拟化的例子中看到的那样，操作系统同时运行着许多事情，首先运行一个进程，然后运行另一个进程，等等。事实证明，这样的做法会引发一些深刻并有趣的问题。

\begin{figure}[ht]
\begin{lstlisting}
#include <stdio.h>
#include <stdlib.h>
#include "common.h"

volatile int counter = 0;
int loops;

void *worker(void *arg) {
    int i;
    for (i = 0; i < loops; i++) {
        counter++;
    }
    return NULL;
}

int
main(int argc, char *argv[])
{
    if (argc != 2) {
        fprintf(stderr, "usage: threads <value>\n");
        exit(1);
    }
    loops = atoi(argv[1]);
    pthread_t p1, p2;
    printf("Initial value : %d\n", counter)
    
    Pthread_create(&p1, NULL, worker, NULL);
    Pthread_create(&p2, NULL, worker, NULL);
    Pthread_join(p1, NULL);
    Pthread_join(p2, NULL);
    printf("Final value : %d\n", counter);
    return 0;
}

\end{lstlisting}
\caption{\textbf{一段多线程程序}}
\end{figure}

不幸的是，并发性问题并不局限于操作系统本身。事实上，现代的\textbf{多线程}程序也存在同样的问题。让我们来演示一段多线程程序的例子（图2.5）。

虽然现在你可能完全不了解这个例子（在本书后面的有关并发的章节中，我们中更多地了解它），但其基本的想法是很简单的。主程序调用 Pthread\_create()\footnote{实际的调用应该是小写的 pthread\_create() ；大写版本是我们对 pthread\_create() 的封装，并确保代码表明调用成功。 详情请参阅代码。}，您可以将线程看作与其他函数在同一内存空间中运行的函数，并且每次都有多个线程处于活动状态。在这个例子中，每个线程开始在一个名为 worker() 的例程中运行，这个例程只是递增循环中的一个计数器 for 的循环次数。

下面是当我们运行这个程序时，变量循环的输入值设置为1000时会发生什么情况的记录。循环的值决定两个线程各自将循环增加共享计数器的次数。当程序以循环值设置为1000运行时，你认为计数器的最终值是多少？

\begin{tcolorbox}[colframe=grey,colback= grey,arc=0pt,left=6pt,right=6pt,top=6pt,bottom=6pt,boxsep=0pt]
\begin{center}
问题的关键\\
如何构建正确的并发程序
\end{center}
当同一个内存空间中由很多并发执行的线程时，我们该如何构建一个能正确工作的程序？操作系统需要哪些原语\footnote{执行过程中不可以被打断的基本操作}？硬件应该提供哪些机制？我们如何使用它们来解决并发问题？
\end{tcolorbox}

\begin{verbatim}
prompt> gcc -o thread thread.c -Wall -pthread 
prompt> ./thread 1000 
Initial value : 0 
Final value   : 2000
\end{verbatim}

你可能猜到了，当两个线程结束时，最终的计数器的值为2000，因为每个线程增加计数器1000次。实际上，当输入的循环值为 N 的时候，我们可以预测到程序的最终输入是 2N。但事实证明，生活不会那么简单。让我们再次运行这个程序，不过给循环设置一个更高的值，看看会发生什么：

\begin{verbatim}
prompt> ./thread 100000 
Initial value : 0 
Final value : 143012 // 嗯？？ 
prompt> ./thread 100000 
Initial value : 0 
Final value : 137298 // 咋了？？
\end{verbatim}

这次运行中，我们将输入的值设为10,0000，但我们得到的最终值却不是20,0000，第一次我们得到了14,3012。然后我们再次运行这个程序，不仅获得了错误的值，而且还与上次的错误值不同。事实上，如果你使用很大的循环值来反复运行程序，你可能会发现偶尔能够得到正确答案。那为什么会这样呢？

事实证明，这些奇怪和不寻常的结果的原因与如何执行指令有关，一次只执行一条指令。不幸的是，上面的程序的一个关键部分，共享计数器递增，需要三条指令：一条是将计数器的值从内存加载到寄存器中，一条是计数器递增，一条是将其存储回内存。因为这三条指令不是\textbf{原子}（一次全部）执行的，就会发生奇怪的事情。这正是我们要在本书的第二部分详细讨论的\textbf{并发性}问题。

\section{持久化}
本课程的第三个主题是\textbf{持久化}。在系统内存中，数据很容易丢失，因为 DRAM 之类的设备以易丢失的方式存储值。当断电或系统奔溃时，内存中的任何数据都会丢失。因此，我们需要硬件或软件持久化的存储数据。这样的存储对于任何系统都很重要，因为用户非常关心他们的数据。

硬件以某种\textbf{输入/输出}或 \textbf{I/O} 设备的形式出现;现代系统中，\textbf{硬盘驱动器}是长期有效信息的通用存储器，尽管\textbf{固态硬盘（SSDs）}也在这个领域取得了进展\footnote{译者注：截止2018年初，固态硬盘已经成为个人及服务器的首选配置，而机械磁盘（及文中的磁盘驱动器）因为容量大、价格低且寿命长，所以在存储领域依然有很大的市场。}。

操作系统中管理磁盘的软件被称为\textbf{文件系统}；因此它负责将用户创建的任何文件以可靠且高效的方式存储在系统的磁盘上。

不同于操作系统为 CPU 和内存提供的抽象，操作系统不会为每个程序创建一个私有的、虚拟的磁盘。相反，还要假定用户需要经常共享文件中的信息。例如，当写 C 语言程序时，你可能要先使用一个编辑器（例如：Emacs\footnote{你应该使用 Emacs，如果你用 vi，那你可能就犯了一些错误。如果你在使用一些其他的不是真的代码编辑器，那就大错特错了。}）创建和编辑 C 文件（emacs -nw main.c）。一旦写完后，你可能用使用编译器将源代码编译成可执行文件（例如：gcc -o main main.c）。当这些完成后，你可能要运行新的可执行文件（例如：./main）。因此你可以看到文件在不同的进程间共享。首先 Emacs 创建一个文件作为编译器的输入；编译器使用输入文件创建出一个新的可执行文件（有很多步骤，可以学习编译器课程学习更多知识）；最终，新的可执行文件被运行了。因此一个新的程序诞生了！

\begin{tcolorbox}[colframe=grey,colback= grey,arc=0pt,left=6pt,right=6pt,top=6pt,bottom=6pt,boxsep=0pt]
\begin{center}
问题的关键\\
如何存储持久化数据
\end{center}
文件系统的操作系统的一部分，负责管理持久化数据。需要哪些正确的技术才能执行此操作？要保证高性能，哪些机制和策略是必要的？面对硬件和软件故障，如何实现可靠性？
\end{tcolorbox}

\begin{figure}[ht]
\begin{lstlisting}
#include <stdio.h>
#include <unistd.h>
#include <assert.h>
#include <fcntl.h>
#include <sys/types.h>
int
main(int argc, char *argv[])
{
    int fd = open("/tmp/file", O_WRONLY | O_CREAT | O_TRUNC, S_IRWXU);
    assert(fd > -1);
    int rc = write(fd, "hello world\n", 13);
    assert(rc == 13);
    close(fd);
    return 0;
}
\end{lstlisting}
\caption{\textbf{一段执行输入/输出的程序}}
\end{figure}

让我们看一些代码来更好的理解文件系统。图2.6展示的代码创建了一个包含"hello world"字符串的文件（/tmp/file）。

为了完成这个任务，程序需要向操作系统发起三次调用。第一步，调用 open() 函数，打开并创建这个文件；第二步，write()，写入一些数据到文件中；第三步，close()，直接关闭文件，表明程序不再向文件中写入任何数据。这些\textbf{系统调用}被路由到操作系统中被称为\textbf{文件系统}的部分，该部分会处理请求并返回某种错误代码给用户。

你可能会好奇，操作系统为了将内容实际写入磁盘而做了什么。我们会告诉过程，但你必须承诺先闭上眼睛；因为这不太愉快。文件系统必须做相当多的工作：首先确定这些新数据会写入到磁盘的哪个位置，然后在文件系统所维护的各种结构中跟踪这些新数据。这样做需要向底层存储设备发出 I/O 请求，以读取现在结构或更新（写入）它们。任何写过\textbf{设备驱动程序}\footnote{设备驱动程序是操作系统中知道如何处理特殊设备的代码。稍后我们会详细的讨论设备和设备驱动程序。}的人都知道，让设备代表用户执行某项操作是一个复杂且详细的过程。这需要深入了解低级设备接口及其确切的语义。幸运的是，操作系统通过自身的系统调用提供的标准且简单的方式来访问设备。因此，操作系统有时被看作成一个\textbf{标准库}。

当然，在如何访问设备及文件系统如何在所述设备上持久化的管理数据上还有很多的细节。处于性能方面的原因，大多数文件系统会首先将写入操作延迟一段时间执行，希望将批量写入操作合并为更大的一组操作。为了处理写入期间系统崩溃的问题，大多数文件系统都包含某种复杂的写入协议，例如\textbf{日志记录}或\textbf{写时复制}，细致排序写入操作到磁盘以确保在写入序列期间发生故障时，系统可以在之后恢复到合理的状态。为了让不同的通用操作更加高效，文件系统使用了许多不同的数据结构和访问方法，从简单的列表到复杂的 B-trees（多路搜索树）。如果所有的这些都没有意义，那好！在本书的第三部分，关于\textbf{持久化}的讨论中，我们会讨论相关的所有细节，我们将在其中讨论设备和 I/O，然后详细的讨论磁盘，磁盘阵列（RAIDs）和文件系统。

\section{设计目标}
现在你对操作系统的实际作了什么应该有一些了解：它需要物理\textbf{资源}，例如 CPU，内存或磁盘，然后将其\textbf{虚拟化}。它处理与\textbf{并发}相关的困难且棘手的问题。并且\textbf{持久化}的存储文件，从而确认文件的长期安全。鉴于我们希望构建这样一个系统，我们需要一些目标，以帮助我们聚焦设计和实施，并在必要的时候作为权衡的标准。找到适合的权衡标准是构建系统的关键。

最基本目标之一是建立一些\textbf{抽象}，让系统方便且易于使用。抽象是我们在计算机科学领域所做的一切的基础。抽象使得编写一个大型程序称为可能：将其分为小而易懂的部分，用 C\footnote{你们中的一些人可能反对将 C 称为高级语言。不过请记住这一个操作系统课程，当然，尽管如此，我们很高兴不必一直用汇编语言来写代码} 这样的高级语言编写这样的程序而不用考虑汇编，用汇编来写代码而不用考虑用逻辑门，构建处理器输出门而不用考虑太多关于晶体管的知识。抽象是如此的根本以至于我们有时会忘记它的重要性，但在这里我们不会；因此，在每个章节中，我们会讨论随着时间推移而发展的一些主要的抽象，给你提供一种方式，来思考操作系统的部分模块。

设计和实现操作系统的一个目标是提供高\textbf{性能}，另一种说法是我们的目标是尽量\textbf{降低操作系统的开销}。虚拟化让操作系统易于使用是非常值得的，但不是不惜任何代价。因此，我们必须努力提供虚拟化和操作系统的其他功能，而不会产生过多的开销。这些开销会以多种形式出现：额外时间（更多指令）和额外空间（内存或磁盘）。如果可能的话，我们会寻求解决方案，尽量减少一种或另外一种甚至二者的开销。但完全并不是总能实现的，我们将学会注意并且（在适当情况下）容忍这些场景。

另一个目标是在应用程序之间以及在操作系统和应用程序之间提供\textbf{保护}。因为我们期望允许许多程序同时运行，所以我们要确保一个程序的恶意或偶然的不良行为不会危害其他程序；我们当然不希望应用程序能够损害操作系统本身（因为这会影响到系统上运行的所有程序）。保护是操作系统基本原理之一的核心，即操作系统的隔离；将进程彼此隔离是保护的关键，因此这也是操作系统必须执行的大部分任务。

操作系统必须不间断的运行；一旦产生运行错误，那么系统上运行的所有应用程序也会失败。由于这种依赖性，操作系统往往力求提供高度的\textbf{可靠性}。随着操作系统变得越来越复杂（有时包含数百万行代码），构建一个可靠的操作系统是一个相当大的挑战，事实上，该领域的许多正在进行的研究（包括我们自己做的一些工作[BS+09, SS+10]）都在关注这个确切的问题。

其他的目标也是有意义的。在我们日益绿色化的世界，\textbf{节能}是很重要的。\textbf{安全性}（保护性的延伸）针对恶意程序非常重要，特别是在这个高度联网的时代；随着操作系统在越来越小的设备上运行，\textbf{移动性}也变的越来越重要。根据系统的使用方式，操作系统将有不同的目标，因此可能会以至少稍微不同的方式实现。但是，正如我们将看到的，我们将介绍的关于如何构建操作系统的许多原则在不同设备的范畴内都是有效的。

\section{一些历史}
在结束介绍前，让我们简要的介绍下操作系统的开发历史。就像任何由人类构建的系统一样，随着时间的推移，好的理念不断在操作系统中积累，工程师们在设计中学到了什么是重要的。这里我们讨论操作系统一些主要的发展过程。要想获得更加丰富的知识，请参阅Brinch Hansen的《操作系统精彩历史》（<excellent history of operating systems>）[BH00]。

\textbf{早期的操作系统：仅仅是库}
一开始，操作系统并没有做太多工作。 基本上，它只是一组常用函数库; 例如，替代每个开发人员去编写底层的输入输出处理代码，“操作系统”会提供这样的API，从而使开发人员的工作更加轻松。

